跳到主要内容

Go 语言学习-异常处理

Go 的异常处理

Go 语言的错误处理机制可以从支持函数多返回值说起:

在 C 语言当中常见的做法是保留一个返回值来表示错误(比如,read() 返回0),或者保留返回值来通知状态,并将传递存储结果的内存地址的指针。这容易产生了不安全的编程实践,因此在像 Go 语言这样有良好管理的语言中是不可行的。认识到这一问题的影响已超出了函数结果与错误通讯的简单需求的范畴,Go的作者们在语言中内建了函数返回多个值的能力。

下面这个是官方的例子,很好的说明了是如何抛出两个返回值,第二个返回值作为异常的:

// Hello returns a greeting for the named person.
func Hello(name string) (string, error) {
// If no name was given, return an error with a message.
if name == "" {
return "", errors.New("empty name")
}

// If a name was received, return a value that embeds the name
// in a greeting message.
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message, nil
}


func main() {
hello, err := Hello("")
if err != nil {
log.Fatalf("error: %v", err)
return
}
fmt.Println(hello)
}
2021/10/21 23:23:02 error: empty name

自定义异常

上面的异常信息只是简单的返回了一个字符串而已,想在报错的时候保留现场,得到更多的异常内容怎么办呢?这就要看看 errors 的内部实现了。其实相当简单。

errors 实现了一个叫 error 的接口,这个接口里就一个 Error 方法且返回一个 string ,如下

go 提供了自带的 errorString 结构体的方法,用于快速抛出异常,使用例如上一节的代码,其实现了 error 接口,如下:

// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}

照葫芦画瓢,所以我们只要扩充下自定义 error 的结构体字段就行了。

这个自定义异常可以在报错的时候存储一些信息,供外部程序使用

type FileError struct {
Op string
Name string
Path string
}

// 实现接口
func (f *FileError) Error() string {
return fmt.Sprintf("路径为 %v 的文件 %v,在 %v 操作时出错", f.Path, f.Name, f.Op)
}

// 初始化函数
func NewFileError(op string, name string, path string) *FileError {
return &FileError{Op: op, Name: name, Path: path}
}

调用这个异常

f := NewFileError("读", "README.md", "/home/how_to_code/README.md")
fmt.Println(f.Error())

打印结果

路径为 /home/how_to_code/README.md 的文件 README.md,在 读 操作时出错

使用 panic 抛出异常

defer 是崩溃后,仍然会被调用的语句,那程序在什么情况下会崩溃呢?

Go 的类型系统会在编译时捕获很多异常,但有些异常只能在运行时检查,如数组访问越界、空指针引用等。这些运行时异常会引起 painc 异常(程序直接崩溃退出)。然后在退出的时候调用当前 goroutine 的 defer 延迟调用语句。

有时候在程序运行缺乏必要的资源的时候应该手动触发宕机(比如配置文件解析出错、依赖某种独有库但该操作系统没有的时候)

defer fmt.Println("关闭文件句柄")

panic("人工创建的运行时异常")

报错如下

不过注意:一些致命性错误不属于 panic

对于官方标准编译器来说,很多致命性错误(比如堆栈溢出和内存不足)不能被恢复。它们一旦产生,程序将崩溃。

使用 recover 捕获异常

出现 panic 以后程序会终止运行,所以我们应该在测试阶段发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的 web 或者 rpc 服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。

然后再借助运维监控系统对日志的监控,发送告警给运维、开发人员,进行紧急修复。

这种时候就可以使用 recover 捕获异常

语法如下:

func divisionIntRecover(a, b int) (ret int) {
defer func() {
if err := recover(); err != nil {
// 打印异常,关闭资源,退出此函数
fmt.Println(err)
ret = -1
}
}()

return a / b
}
// 测试除 0
divisionIntRecover(10, 0)
  • 调用 panic 后(除 0 bug),当前函数从调用点直接退出
  • recover 函数只有在 defer 代码块中才会有效果
  • recover 可以放在最外层函数,做统一异常处理。

但是注意,recover 捕获的时机,如下代码所示:

func main() {
doTest()
fmt.Println("main捕获到错误:", recover())
}

func doTest() {
defer func() {
fmt.Println("1捕获到错误:", recover())
}()
fmt.Println("2捕获到错误:", recover())
panic("报错3")
}

打印:

2捕获到错误: <nil>
1捕获到错误: 报错3
main捕获到错误: <nil>

总结:Go 中的错误处理流程

在异常处理方面,Go 语言不像其他语言,使用 try..catch.. finall..., 而使用 defer, panic, recover,将异常和控制流程区分开。即通过 panic 抛出异常,然后在 defer 中,通过 recover 捕获这个异常,最后处理。

如下:

func main() {
//立即执行函数
defer func() { // 声明defer,
fmt.Println("----调用 defer1 start----")
if err := recover(); err != nil {
fmt.Println(err) // 这里的err其实就是panic传入的内容
}
fmt.Println("----调用 defer1 end----")
}()

defer func() { // 声明defer,
fmt.Println("----调用 defer2 start----")
if err := recover(); err != nil {
fmt.Println(err) // 这里的err其实就是panic传入的内容
}
fmt.Println("----调用 defer2 end----")
}()

panic("测试")
}

输出:

----调用 defer2 start----
测试
----调用 defer2 end----
----调用 defer1 start----
----调用 defer1 end----

panic 是用来表示非常严重的不可恢复的错误的。在 Go 语言中这是一个内置函数,接收一个 interface{} 类型的值(也就是任何值了)作为参数。

panic 的作用就像我们平常接触的异常,所以,调用 panic 会让程序不会继续往下执行(除非 recover)。

recover 捕获异常后的异常,不能再次被 recover 捕获。

References

Return and handle an error Go语言中的异常和错误处理简介 Go 语言的错误处理机制是一个优秀的设计吗? - 茹姐的回答 - 知乎 golang异常处理详解 - 机智的程序员小熊的文章 - 知乎